#include "context.h"
#include "link.h"
#include "utils.h"
#include <QtCore/qmath.h>
#include <QDir>
#include <QPainter>

Context::Context(const Point & center, const QSize & displaySize, QObject *parent):
QThread(parent),
pixmapItem(NULL),
minePixmap(9, 19),
zoomInItem(NULL),
zoomOutItem(NULL),
doStop(true)
{
    options.viewPortSize = displaySize;
    options.viewCenter = center;
    options.viewCenter.normalize();
    options.viewPercent = 0.1;
    options.angle = 0.0;
    options.scale = 1.0;
    options.cachePercent = 2.0;
    options.mapDetalization = MapElement::ALL;
    options.antialising = true;
    lastOptions = options;
    mapScene = new Scene(this);
    pixmapItem = new MapPixmapItem();
    connect(pixmapItem, SIGNAL(moved(qreal, qreal)), this, SLOT(handleMove(qreal, qreal)));
    mapScene->addItem(pixmapItem);
    connect(mapScene, SIGNAL(zoomed(qreal, const QPointF &)), this, SLOT(handleZoom(qreal, const QPointF &)));
    connect(this, SIGNAL(imageReady()), this, SLOT(updateScene()));

    updateCache();
    updateScale();
    initTestMap();
    start();
    draw();
    createTouchMenu();
}

Context::~Context()
{
    stop();
    while(isRunning())
        continue;
    delete mapScene;
    mapScene = NULL;
}

void Context::stop()
{
    QWriteLocker wLocker(&rwLock);
    doStop = true;
    paintWait.wakeOne();
}

void Context::createTouchMenu()
{
    zoomInItem = new TouchItem(0, 0, "+");
    zoomOutItem = new TouchItem(0, 0, "-");
    mapScene->addItem(zoomInItem);
    mapScene->addItem(zoomOutItem);
    connect(zoomInItem, SIGNAL(clicked()), this, SLOT(zoomIn()));
    connect(zoomOutItem, SIGNAL(clicked()), this, SLOT(zoomOut()));
    rotateClockwiseItem = new TouchItem(0, 0, ">");
    rotateAnticlockwiseItem = new TouchItem(0, 0, "<");
    mapScene->addItem(rotateClockwiseItem);
    mapScene->addItem(rotateAnticlockwiseItem);
    connect(rotateClockwiseItem, SIGNAL(clicked()), this, SLOT(rotateClockwise()));
    connect(rotateAnticlockwiseItem, SIGNAL(clicked()), this, SLOT(rotateAnticlockwise()));
    arrangeTouchMenu();
}

void Context::arrangeTouchMenu()
{
    zoomInItem->setPos(0, options.viewPortSize.height()/2 - 40);
    zoomOutItem->setPos(0, options.viewPortSize.height()/2 + 1);
    rotateClockwiseItem->setPos(options.viewPortSize.width() - 40, options.viewPortSize.height()/2 - 40);
    rotateAnticlockwiseItem->setPos(options.viewPortSize.width() - 40, options.viewPortSize.height()/2 + 1);
}

const Point & Context::center() const
{
    return options.viewCenter;
}

inline double Context::cacheRadius() const
{
    return options.viewPercent * (1.0 + options.cachePercent) * 90.0;
}

const Context::Options & Context::getOptions() const
{
    return options;
}

void Context::resize(const QSize & size)
{
    if(options.viewPortSize != size)
    {
        options.viewPortSize = size;
        arrangeTouchMenu();
        draw();
    }
}

void Context::move(double dlatitude, double dlongitude)
{
    moveTo(qMin(89.9, qMax(-89.9, options.viewCenter.getLatitude() + dlatitude)), options.viewCenter.getLongitude() + dlongitude);
}

void Context::moveTo(double latitude, double longitude)
{
    options.viewCenter = Point(latitude, longitude);
    options.viewCenter.normalize();

    updateCache();
    draw();
}

void Context::handleMove(qreal dx, qreal dy)
{
    qreal _dx = dx * qCos(Utils::toRadians(options.angle)) + dy * qSin(Utils::toRadians(options.angle));
    qreal _dy = dx * qSin(Utils::toRadians(options.angle)) - dy * qCos(Utils::toRadians(options.angle));
    move(-180.0 * _dy / (M_PI * options.scale * Utils::Radius), -180.0 * _dx / (M_PI * options.scale * Utils::Radius * qCos(options.viewCenter.getRadianLatitude())));
//    qDebug("moved to %f, %f", options.viewCenter.getLatitude(), options.viewCenter.getLongitude());
}

void Context::handleZoom(qreal dscale, const QPointF & constPoint)
{
    options.viewPercent = qMin(1.0, options.viewPercent * dscale);
    qDebug("zooming to %f\%", options.viewPercent * 100.0);
//    updateCache();

//    if(isFlat())
//    {
//        updateScale();
//        handleMove(-(1.0 - dscale) * (constPoint.x() - options.viewPortSize.width()/2.0), -(1.0 - dscale) * (constPoint.y() - options.viewPortSize.height()/2.0));
//    }
//    else
//    {
        updateScale();
        draw();
//    }
}

void Context::zoomIn()
{
    handleZoom(1.0/2.0, QPoint(options.viewPortSize.width()/2.0, options.viewPortSize.height()/2.0));
}

void Context::zoomOut()
{
    handleZoom(2.0, QPoint(options.viewPortSize.width()/2.0, options.viewPortSize.height()/2.0));
}

void Context::rotateClockwise()
{
    options.angle += 10.0;
    draw();
}

void Context::rotateAnticlockwise()
{
    options.angle -= 10.0;
    draw();
}

void Context::updateAntialiasing(int checkBoxState)
{
    options.antialising = (checkBoxState == Qt::Checked);
    draw();
}

void Context::updateBuildingShow(int checkBoxState)
{
    if (checkBoxState == Qt::Checked)
        options.mapDetalization |= MapElement::BUILDING;
    else
        options.mapDetalization &= ~MapElement::BUILDING;
    updateCache();
    draw();
}

void Context::updateWayShow(int checkBoxState)
{
    if (checkBoxState == Qt::Checked)
        options.mapDetalization |= MapElement::WAY;
    else
        options.mapDetalization &= ~MapElement::WAY;
    updateCache();
    draw();
}

void Context::updateScale()
{
    options.scale = qMin(options.viewPortSize.width(), options.viewPortSize.height()) / (2.0 * Utils::Radius * options.viewPercent);
}

void Context::updateCache()
{
    quint32 detalization = MapElement::NONE;
    if(options.scale > 4.10331e-06)
        detalization |= MapElement::WAY;
    dataCache.update(options.viewCenter, cacheRadius(), detalization & options.mapDetalization);
}

int Context::x0(const Point & p, const Point & center, qreal scale, qreal angle)
{
    qreal w = Utils::shortestDistance(p, Point(0, 0, -Utils::Radius), Point(90.0, 0), center);
    qreal h = Utils::shortestDistance(p, Point(0, 0, -Utils::Radius), center, Point(0, center.getLongitude() + 90.0));
    return w * scale * qCos(Utils::toRadians(-angle)) - h * scale * qSin(Utils::toRadians(-angle));
}

int Context::y0(const Point & p, const Point & center, qreal scale, qreal angle)
{
    qreal w = Utils::shortestDistance(p, Point(0, 0, -Utils::Radius), Point(90.0, 0), center);
    qreal h = Utils::shortestDistance(p, Point(0, 0, -Utils::Radius), center, Point(0, center.getLongitude() + 90.0));
    return -(h * scale * qCos(Utils::toRadians(-angle)) + w * scale * qSin(Utils::toRadians(-angle)));
}

int Context::x(const Point & p, const Point & center, qreal scale, qreal angle, short viewPortWidth, short xshift)
{
    return viewPortWidth / 2.0 + x0(p, center, scale, angle) + xshift;
}

int Context::y(const Point & p, const Point & center, qreal scale, qreal angle, short viewPortHeight, short yshift)
{
    return viewPortHeight / 2.0 + y0(p, center, scale, angle) + yshift;
}

void Context::initTestMap()
{
    for(QMap<quint64, Junction *>::iterator it = junctions.begin(); it != junctions.end(); it++)
    {
        delete it.value();
    }
    junctions.clear();

    for(QMap<quint64, Way *>::iterator it = ways.begin(); it != ways.end(); it++)
    {
        delete it.value();
    }
    ways.clear();

    QDir dir("./map");
    QStringList dirEntries = dir.entryList();
    int minLatitude = INT_MAX;
    int minLongitude = INT_MAX;
    int maxLatitude = 1 - INT_MAX;
    int maxLongitude = 1 - INT_MAX;
    Junction * junction = NULL;
    int blockCode = 0;
    int bottomLeftLatitude;
    int bottomLeftLongtitude;
    int vsize = 0;
    int hsize = 0;
    int jLatitude = 0;
    int jLongitude = 0;
    quint64 jSrcId = 0;
    quint64 jDstId = 0;
    int distance = 0;
    quint8 vehicleType = 0;
    quint8 quality = 0;
    Junction * srcJunction = NULL;
    Junction * dstJunction = NULL;
    QStringList filters;
    foreach(QString entry, dirEntries)
    {
        qDebug(entry.toAscii());
        filters.clear();
        if(entry == "junctions")
        {
            quint64 jId = 0;
            filters << "*.junc";
            dir.cd("junctions");
            QStringList junctionBlocks = dir.entryList(filters);
            foreach(QString blockName, junctionBlocks)
            {
                qDebug(blockName.toAscii());
                QFile block("map/junctions/" + blockName);
                if(block.open(QIODevice::ReadOnly))
                {
                    qDebug("opened");
                    QDataStream in(&block);
                    in >> blockCode >> bottomLeftLatitude >> bottomLeftLongtitude >> vsize >> hsize;
                    while(!in.atEnd())
                    {
                        in >> jId;
                        if(jId == 0)
                            break;
                        in >> jLatitude >> jLongitude;
                        junction = new Junction(jId, jLatitude / 10000000.0, jLongitude / 10000000.0);
                        minLatitude = qMin(minLatitude, jLatitude);
                        minLongitude = qMin(minLongitude, jLongitude);
                        maxLatitude = qMax(maxLatitude, jLatitude);
                        maxLongitude = qMax(maxLongitude, jLongitude);
                        junctions.insert(jId, junction);
                    }
                    while(!in.atEnd())
                    {
                        in >> jSrcId >> jDstId >> distance >> vehicleType >> quality;
                        srcJunction = junctions.value(jSrcId);
                        dstJunction = junctions.value(jDstId);
                        if (dstJunction != NULL)
                        {
                            Link link(distance, vehicleType, quality);
                            srcJunction->addNeightbour(dstJunction, link);
                        }
                    }
                }
            }
            dir.cdUp();
        } else if(entry == "routes")
        {
            quint64 wId = 0;
            quint8 tag = 0;
            quint8 length = 0;
            QVector<quint8> value;
            quint32 nodesCount = 0;
            quint64 nId = 0;
            quint32 nLatitude = 0;
            quint32 nLongitude = 0;
            filters << "*.route";
            dir.cd("routes");
            QStringList waysBlocks = dir.entryList(filters);
            foreach(QString blockName, waysBlocks)
            {
                qDebug(blockName.toAscii());
                QFile block("map/routes/" + blockName);
                if(block.open(QIODevice::ReadOnly))
                {
                    qDebug("opened");
                    QDataStream in(&block);
                    in >> blockCode >> bottomLeftLatitude >> bottomLeftLongtitude >> vsize >> hsize;
                    while(!in.atEnd())
                    {
                        in >> wId;
                        Way * way = new Way(wId);
                        for(in >> tag; tag != 0; in >> tag)
                        {
                            in >> length;
                            value.resize(length);
                            for(quint8 i = 0; i < length; i++)
                                in >> value[i];
                        }
                        value.clear();
                        in >> nodesCount;
                        for(uint i = 0; i < nodesCount; i++)
                        {
                            in >> nId >> nLatitude >> nLongitude;
                            Node * node = new Node(nId, nLatitude / 10000000.0, nLongitude / 10000000.0);
//                            qDebug(QString("importing node %1, %2").arg(node->getLatitude()).arg(node->getLongitude()).toAscii());
                            way->addNode(node);
                        }
                        ways.insert(wId, way);
                    }
                }
            }
            dir.cdUp();
        }
    }
//    scale = qMin(480.0 / (maxLongitude - minLongitude), 640.0 / (maxLatitude - minLatitude) );
//    moveTo(minLatitude, minLongitude);

    QPainter painter(&minePixmap);
    painter.setPen(Qt::darkMagenta);
    painter.eraseRect(0, 0, minePixmap.width(), minePixmap.height());
    painter.drawLine(0, 0, 4, 2);
    painter.drawLine(4, 2, 8, 0);
    painter.drawLine(8, 0, 4, 19);
    painter.drawLine(4, 19, 0, 0);
}

bool Context::isFlat() const
{
    return options.viewPercent <= 0.125;
}

bool Context::needRepaint()
{
    QReadLocker rLocker(&rwLock);
    return pixmapItem == NULL
            || options.angle != lastOptions.angle
            || options.viewPercent != lastOptions.viewPercent
            || options.viewPortSize != lastOptions.viewPortSize
            || options.antialising != lastOptions.antialising
            || options.mapDetalization != lastOptions.mapDetalization
            || Utils::angleDistance(lastOptions.viewCenter, options.viewCenter) / 90.0 + options.viewPercent > lastOptions.viewPercent * (1.0 + lastOptions.cachePercent / 2.0);
}

void Context::repaint()
{
    QWriteLocker wLocker(&rwLock);
    optionsQueue.enqueue(options);
    paintWait.wakeOne();
}

void Context::drawGrid(QPaintDevice * paintDevice, const Options & drawOptions)
{
    QPainter painter(paintDevice);
    painter.setBackgroundMode(Qt::OpaqueMode);
    painter.setBackground(QBrush(QColor("black")));
    painter.eraseRect(0, 0, paintDevice->width(), paintDevice->height());
    painter.setPen(QPen(Qt::lightGray));
    painter.setRenderHint(QPainter::Antialiasing, drawOptions.antialising);
    Point p, vp, hp;
    hp.setLongitude(0);
    for(double lon = 0.0; lon <= 270.0; lon += 10.0)
    {
        vp.setLongitude(lon);
        vp.setLatitude(-90.0);
        for(double lat = -80.0; lat <= 70.0; lat += 10.0)
        {
            p.setLatitude(lat);
            p.setLongitude(lon);
            hp.setLatitude(lat);
            painter.drawLine(x(vp, drawOptions.viewCenter, drawOptions.scale, drawOptions.angle, drawOptions.viewPortSize.width()),
                             y(vp, drawOptions.viewCenter, drawOptions.scale, drawOptions.angle, drawOptions.viewPortSize.height()),
                             x(p, drawOptions.viewCenter, drawOptions.scale, drawOptions.angle, drawOptions.viewPortSize.width()),
                             y(p, drawOptions.viewCenter, drawOptions.scale, drawOptions.angle, drawOptions.viewPortSize.height()));
            painter.drawLine(x(hp, drawOptions.viewCenter, drawOptions.scale, drawOptions.angle, drawOptions.viewPortSize.width()),
                             y(hp, drawOptions.viewCenter, drawOptions.scale, drawOptions.angle, drawOptions.viewPortSize.height()),
                             x(p, drawOptions.viewCenter, drawOptions.scale, drawOptions.angle, drawOptions.viewPortSize.width()),
                             y(p, drawOptions.viewCenter, drawOptions.scale, drawOptions.angle, drawOptions.viewPortSize.height()));
            vp.setLatitude(lat);
        }
        hp.setLongitude(lon);
    }
}

void Context::draw()
{
    if(!isFlat())
    {
        QPixmap pixmap(options.viewPortSize);
        drawGrid(&pixmap, options);
        QWriteLocker wLocker(&rwLock);
        qDebug("drawing grid");
        pixmapItem->setPixmap(pixmap);
        pixmapItem->setPos(0, 0);
//        imagePtr.clear();
    }
    else if(needRepaint())
    {
//        qDebug("repainting");
        repaint();
    }
    if(pixmapItem != NULL && isFlat())
    {
//        qDebug("setting pos");
        QWriteLocker wLocker(&rwLock);
        QSize shift = lastOptions.viewPortSize * lastOptions.cachePercent / 2.0;
        pixmapItem->setPos(x0(lastOptions.viewCenter, options.viewCenter, options.scale, options.angle) - shift.width(),
                           y0(lastOptions.viewCenter, options.viewCenter, options.scale, options.angle) - shift.height());
    }
}

void Context::run()
{
    QSharedPointer<QImage> image = QSharedPointer<QImage>(NULL);
    Options curOptions;
    QSize shift;
    {
        QWriteLocker wLocker(&rwLock);
        doStop = false;
    }
    forever
    {
        {
            QWriteLocker wLocker(&rwLock);
            while(!doStop && optionsQueue.empty())
                paintWait.wait(&rwLock);
            if(doStop)
                break;
            while(optionsQueue.size() > 1)
                optionsQueue.erase(optionsQueue.begin(), optionsQueue.end() - 1);
            curOptions = optionsQueue.dequeue();
        }
        shift = curOptions.viewPortSize * curOptions.cachePercent / 2.0;
        image = QSharedPointer<QImage>(new QImage(curOptions.viewPortSize * (1.0 + curOptions.cachePercent), QImage::Format_ARGB32));
        QPainter painter(image.data());
        painter.setBackgroundMode(Qt::OpaqueMode);
        painter.setBackground(QBrush(QColor("white")));
        painter.eraseRect(0, 0, image.data()->width(), image.data()->height());
        painter.setRenderHint(QPainter::Antialiasing, curOptions.antialising);

        if((curOptions.mapDetalization & MapElement::WAY) > 0)
        {
            Way * way = NULL;
            Node * prevNode = NULL;
            painter.setPen(QPen(QBrush(Qt::black), 7, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
            for(QMap<quint64, Way *>::iterator it = ways.begin(); it != ways.end(); it++)
            {
                way = it.value();
                int i = 0;
                foreach(Node * node, way->getNodes())
                {
                    if(i++ != 0)
                    {
                        painter.drawLine(x(*prevNode, curOptions.viewCenter, curOptions.scale, curOptions.angle, curOptions.viewPortSize.width(), shift.width()),
                                         y(*prevNode, curOptions.viewCenter, curOptions.scale, curOptions.angle, curOptions.viewPortSize.height(), shift.height()),
                                         x(*node, curOptions.viewCenter, curOptions.scale, curOptions.angle, curOptions.viewPortSize.width(), shift.width()),
                                         y(*node, curOptions.viewCenter, curOptions.scale, curOptions.angle, curOptions.viewPortSize.height(), shift.height()));
    //                    qDebug((QString::number(x1) + "," + QString::number(y1) + "->" + QString::number(x2) + "," + QString::number(y2)).toAscii());
    //                    painter.drawLine(x1, y1, x2, y2);
                    }
                    prevNode = node;
                }
            }
            painter.setPen(QPen(QBrush(Qt::magenta), 6, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
            for(QMap<quint64, Way *>::iterator it = ways.begin(); it != ways.end(); it++)
            {
                way = it.value();
                int i = 0;
                foreach(Node * node, way->getNodes())
                {
                    if(i++ != 0)
                    {
                        painter.drawLine(x(*prevNode, curOptions.viewCenter, curOptions.scale, curOptions.angle, curOptions.viewPortSize.width(), shift.width()),
                                         y(*prevNode, curOptions.viewCenter, curOptions.scale, curOptions.angle, curOptions.viewPortSize.height(), shift.height()),
                                         x(*node, curOptions.viewCenter, curOptions.scale, curOptions.angle, curOptions.viewPortSize.width(), shift.width()),
                                         y(*node, curOptions.viewCenter, curOptions.scale, curOptions.angle, curOptions.viewPortSize.height(), shift.height()));
    //                    qDebug((QString::number(x1) + "," + QString::number(y1) + "->" + QString::number(x2) + "," + QString::number(y2)).toAscii());
    //                    painter.drawLine(x1, y1, x2, y2);
                    }
                    prevNode = node;
                }
            }
        }
        QWriteLocker wLocker(&rwLock);
        imagePtr = image;
        emit imageReady();
        lastOptions = curOptions;
    }
}

void Context::updateScene()
{
    {
        QWriteLocker wLocker(&rwLock);
        pixmapItem->setPixmap(QPixmap::fromImage(*imagePtr.data()));
        imagePtr.clear();
    }
    draw();
}

Scene * Context::scene() const
{
    return mapScene;
}
